"""
Provides access to an Object Server.

Example:
    import ObjectServerDB
    ObjectServerDB.connect("netcool1")
"""

## TODO
## Make connect() accept names from the interfaces file
## Make JDBC Url a kw arg


from com.ziclix.python.sql import zxJDBC
from java.lang import Class
from java.sql import DriverManager
from java.util import HashSet
from java.util import Properties
import java.sql.Connection
import java.sql.DatabaseMetaData

from util import any, all


zx_attribs = ("apilevel", "threadsafety", "paramstyle",
              "Date", "DateFromTicks", "Time", "TimeFromTicks", "Timestamp", "TimestampFromTicks",
              "Binary", "BINARY")
exception_attrs = ("Error", "Warning", "InterfaceError", "DatabaseError", "OperationalError",
                   "IntegrityError", "InternalError", "ProgrammingError", "NotSupportedError")
for attr in zx_attribs + exception_attrs:
    globals()[attr] = getattr(zxJDBC, attr)


## type codes
# The DB API (pep0249) specifies that all types that are returned in a
# cursor description should compare equal to one of STRING, BINARY,
# NUMBER, DATETIME, or ROWID. The zxJDBC class exports the Java SQL
# types codes from java.sql.Types, but these are integers that don't
# always compare equal to one of the types from the Python DB api.
#
# Type codes are mapped from the Object Server, through Java, to
# Jython like so:
# 
# | OS Type    | Java Type                              | Python Type |
# |------------+----------------------------------------+-------------|
# | INTEGER    | INTEGER                                | NUMBER      |
# | INCR       | INTEGER                                | NUMBER      |
# | UNSIGNED   | INTEGER                                | NUMBER      |
# | BOOLEAN    | BOOLEAN, BIT                           | NUMBER      |
# | REAL       | DECIMAL, DOUBLE, FLOAT,                | NUMBER      |
# | TIME       | TIME, TIMESTAMP, DATETIME              | DATETIME    |
# | CHAR       | CHAR, NCHAR                            | STRING      |
# | VARCHAR    | STRING, VARCHAR, NVARCHAR, LONGVARCHAR | STRING      |
# | INTEGER64  | BIGINT                                 | NUMBER      |
# | UNSIGNED64 | BIGINT                                 | NUMBER      |
#

_type_object = dict()    
class DBAPITypeObject:
    def __init__(self, attrs):
        attrs = attrs.split()
        self.values = [getattr(zxJDBC, a) for a in attrs]
        _type_object.update(dict.fromkeys(self.values, self))

    def __cmp__(self,other):
        if other in self.values:
            return 0
        if other < self.values:
            return 1
        else:
            return -1


STRING = DBAPITypeObject("STRING CHAR NCHAR VARCHAR NVARCHAR LONGVARCHAR")
NUMBER = DBAPITypeObject("BIGINT INTEGER BOOLEAN DECIMAL DOUBLE FLOAT BIT")
DATETIME = DBAPITypeObject("DATETIME TIME TIMESTAMP")
ROWID = DBAPITypeObject("")



def connect(server, port=4100, username="root", password="", **kw):
    """
    Connect to an Object Server
    """
    kw.update(locals())
    del kw["kw"]
    dbconn = zxJDBC.connect(ObjServerConn(**kw))
    return ObjServerConnection(dbconn)


class ObjServerConnection(object):
    """
    Wraps zxJDBC connection adding extension methods and exceptions.
    """
    def __init__(self, connection):
        # Exceptions as connection attributes
        self.__dict__.update(dict([(attr, getattr(zxJDBC, attr)) for attr in exception_attrs]))
        self.connection = connection

    def __getattr__(self, n):
        return getattr(self.connection, n)

    def cursor(self):
        return ObjServerCursor(self.connection.cursor())

    
def normalize(i):
    """
    Remove unexpected null terminating chars
    """
    try:
        if i.endswith('\x00'):
            return i[:-1]
        else:
            return i
    except AttributeError:
        return i

class ObjServerCursor(object):
    """
    Cursor class that strips null terminators from field values.
    """
    def __init__(self, cursor):
        self.cursor = cursor
        
    def __getattr__(self, n):
        return getattr(self.cursor, n)
    
    def __iter__(self):
        return self

    def set_arraysize(self, v):
        self.cursor.arraysize = v
    arraysize = property(lambda s: s.cursor.arraysize, set_arraysize)

    def fetchone(self):
        row = self.cursor.fetchone()
        if row == None:
            return None
        return tuple([normalize(v) for v in row])

    def fetchmany(self, size=None):
        if size is None:
            size = self.cursor.arraysize
        ret = []
        for row in self.cursor.fetchmany(size):
            ret.append(tuple([normalize(v) for v in row]))
        return ret

    def fetchall(self):
        rows = self.cursor.fetchall()
        if rows == None:
            return None
        ret = []
        for row in rows:
            ret.append(tuple([normalize(v) for v in row]))
        return ret

    def next(self):
        row = self.fetchone()
        if row == None:
            raise StopIteration
        return row

    def description(self):
        if self.cursor.description:
            return [(name, _type_object[type_code], display_size, internal_size, precision, scale, null_ok)
                    for (name, type_code, display_size, internal_size, precision, scale, null_ok)
                    in self.cursor.description]
        # execute has not been called, return whatever it was that
        # tested false
        return self.cursor.description
    description = property(description)

    def execute(self, operation, params=()):
        if no_quotes(params):
            return self.cursor.execute(operation, params)
        else:
            # we've been passed a param with a ' in it, try to do subsitution
            if operation.count("?") != len(params):
                raise ValueError("Cannot substitute %i parameters into %r" % (len(params), operation))
            return self.cursor.execute(lescape(operation, params))

    def executemany(self, operation, manyparams=()):
        if all([no_quotes(params) for params in manyparams]):
            return self.executemany(operation, manyparams)
        else:
            for params in manyparams:
                self.execute(operation, params)


def no_quotes(params):
    "true if there are no strings containing a ' in seq"
    return all([p.count("'") == 0 for p in params if isinstance(p, basestring)])


def lescape(operation, params):
    """
    Return operation with param placeholders replaced with literals
    """
    def escape(p):
        if isinstance(p, basestring):
            return "'%s'" % p.replace("'", r"\'")
        return p
    parts = operation.split("?")
    eparams = [escape(p) for p in params]
    return "".join(["%s%s" % (a, b) for a,b in zip(parts, eparams)] + parts[-1:])
            
    

class ObjServerConn(java.sql.Connection):
    """
    A JDBC connection to an ObjectServer.

    We cannot connect to the object server using the sybase drivers
    and zxJDBC as it does not support database metadata. This creates
    a connection with a suitable DatabaseMetaData object that will
    allow us to connect.

    """
    def __init__(self, server, port=4100, username=None, password=None, **kw):
        # this is the Netcool/Sybase jConnect driver in jconn2.jar
#        self.driver = "com.sybase.jdbc2.jdbc.SybDriver"
        # this is the Netcool/Sybase driver used by nco_confpack
        # it allows you to get fields longer than 255 chars
        self.driver = "com.sybase.jdbc3.jdbc.SybDriver"
        self.server = server
        self.username = username
        self.password = password
        self.port = port
        self.url = "jdbc:sybase:Tds:%(server)s:%(port)s" % locals()

        kw["user"] = username
        kw["password"] = password
        if not kw.has_key("literal_params"):
            kw["LITERAL_PARAMS"] = "true"
        self.connection = jdbcconnect(self.driver, self.url, jproperties(kw))
        self.metadata = ObjServerMetaData()

    def getMetaData(self):
        return self.metadata

    def __getattr__(self, attr):
        return getattr(self.connection, attr)

def jproperties(dict):
    "java.util.Properties from dict"
    p = Properties()
    for k,v in dict.items():
        p.setProperty(k, v)
    return p

class ObjServerMetaData(java.sql.DatabaseMetaData):
    """
    Database Metadata object that describes the object server.
    """
    def supportsTransactions(self):
        return False

    def supportsStoredProcedures(self):
        return False

def jdbcconnect(driver, url, props):
    """
    A JDBC connection.
    """
    Class.forName(driver)     # load the driver

    _trace = False
    if _trace:
        # Tracing raises a _lot_ of metadata exceptions
        from java.lang import System
        from java.io import FileWriter, PrintWriter
        pwTrace = PrintWriter(System.out)
        DriverManager.setLogWriter(pwTrace)
    return DriverManager.getConnection(url, props)



